
#region Imported Namespaces
using System;
using System.Collections;
using System.Configuration;
using System.EnterpriseServices;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using System.Xml;
using Microsoft.Win32;
using ASPTypeLibrary;

using Tracer = PHSRAG.Utility.Tracer;
#endregion

namespace PHSRAG.Security
{
	#region Class SSO
	/// <summary>
	/// The SSO class provides Single-Sign-On facilities.
	/// This class can only be used in the context of a Web application.
	/// </summary>
	/// <remarks>
	/// One of the issues in creating an object which must work both from the ASP and ASP.NET contexts is the fact that
	/// the HTTP Request/Response objects are NOT the same between ASP and ASP.NET.
	/// However, the .NET Enterprise Services allows an object from within the .NET world to access the Request/Response
	/// objects in the ASP'world using the "ContextUtil" interface simply by marking the class with the IISIntrinsics
	/// attribute set to true.
	/// We also have to pull in the type library from ASP.DLL by adding a reference to Windows\System32\Inetsrv\Asp.dll.
	/// </remarks>
	[Guid("94354C27-3AEF-46ba-AA44-D22CA63B0893"), IISIntrinsics(true)]
	public class SSO
	{
		#region Class Context
		/// <summary>
		/// The Context class encapsulates three things that define a valid login context: the loginID of a user, the
		/// last time that user logged-in, and the outstanding login count. Typically, an instance of this object is
		/// stored with the associated application name as its key (and hence, the application name itself is not stored
		/// in the Context).
		/// </summary>
		private class Context
		{
			#region Instance Variables
			private int			count;
			private string		loginID;
			private DateTime	timeStamp;
			#endregion

			#region Public Properties
			/// <summary>Get/Set the login count</summary>
			public int Count { get { return count; } set { count = value; } }

			/// <summary>Get/Set the login ID</summary>
			public string LoginID { get { return loginID; } set { loginID = value; } }

			/// <summary>Get/Set the last time the user logged-in</summary>
			public DateTime TimeStamp { get { return timeStamp; } set { timeStamp = value; } }
			#endregion

			#region Constructor
			/// <summary>
			/// Initialize the state of this instance.
			/// </summary>
			/// <param name="loginID">Login ID of a user</param>
			/// <param name="timeStamp">Time of login</param>
			public Context(string loginID, DateTime timeStamp, int count)
			{
				this.loginID = loginID;
				this.timeStamp = timeStamp;
				this.count = count;
			}
			#endregion

			#region Public Methods
			/// <summary>
			/// Provide a user-friendly formatted string showing the state of this instance.
			/// </summary>
			/// <returns>String of form "login-id|time-stamp|count"</returns>
			public override string ToString()
			{
				return string.Format("{0}|{1}|{2}", loginID, timeStamp, count);
			}
			#endregion
		}
		#endregion

		#region Constants
		private const string CookieKey = "PHSRAG_SSO_1C2F334B-CF3E-481d-99F7-23AA812498E5";
		private const string CountKey = "Count";
		private const string LoginIDKey = "LoginID";
		private const string RegistryKey = @"Software\PHSRAG\SSO";
		private const string TimeStampKey = "TimeStamp";
		#endregion

		#region Instance Variables
		private int		cookieLifeTime = 1440;	// 24 hours (in minutes)
		private bool	enable = true;
		private int		loginLifeTime = 60;	// Minutes
		#endregion

		#region Constructor
		public SSO()
		{
			LoadConfiguration();
			Tracer.Trace(string.Format("SSO: loginLifeTime: {0}", loginLifeTime));
		}
		#endregion

		#region Public Prperties
		public bool Enable { get { return enable; } set { enable = value; } }
		#endregion

		#region Public Methods
		/// <summary>
		/// This method marks the specified login ID as logged-in to the specified application.
		/// </summary>
		/// <remarks>
		/// Ensure that the calling context is a Web application. Retrieve the login context for the specified application.
		/// If no such context exists (implying that no user has yet logged in to that application), create a new context.
		/// Increment the login count and set the time-stamp to current time. Save the context.
		/// </remarks>
		/// <param name="application">Application to login to</param>
		/// <param name="loginID">Login ID of a user</param>
		public void Enter(string application, string loginID)
		{
			Tracer.Trace(string.Format("Enter: Application: {0}, LoginID: {1}, Enabled: {2}", application, loginID, enable));
			if (enable)
			{
				Context context = GetContext(application);
				if (context == null)
					context = new Context(loginID, DateTime.Now, 1);
				else
				{
					++context.Count;
					context.LoginID = loginID;
					context.TimeStamp = DateTime.Now;
				}

				SetContext(application, context);
			}
		}

		/// <summary>
		/// Get the login ID of the user who most recently logged-in to the specified application.
		/// </summary>
		/// <remarks>
		/// Ensure that the calling context is a Web application. Retrieve the login context for the specified application.
		/// If the context exists (implying that a user had logged in to that application), verfity that the last login
		/// time is not older than the allowable life time of the context. If it is not, the login ID from the retrieved
		/// context will be returned. Otherwise, null is returned.
		/// </remarks>
		/// <param name="application">Application key</param>
		/// <returns>Login ID if found and current, null otherwise</returns>
		public string GetLogin(string application)
		{
			Tracer.Trace(string.Format("GetLogin: Application: {0}, Enabled: {1}", application, enable));
			if (enable)
			{
				Context context = GetContext(application);
				if (context != null)
				{
					Tracer.Trace(context.ToString());

					if (context.Count > 0)
					{
						TimeSpan timeSpan = DateTime.Now.Subtract(context.TimeStamp);
						if (timeSpan.TotalMinutes <= loginLifeTime)
							return context.LoginID;
						Tracer.Trace(string.Format("Login for {0} has timed-out.", context.LoginID));
					}
				}
			}
			return null;
		}

		/// <summary>
		/// This method marks the specified login ID as logged-out of the specified application. Every such action will
		/// render the login count to decrement (the count never goes less than 0).
		/// </summary>
		/// <remarks>
		/// Ensure that the calling context is a Web application. Retrieve the login context for the specified application.
		/// If the context exists (implying that a user had logged in to that application) decrement the login count.
		/// Save the context.
		/// </remarks>
		/// <param name="application">Application to login to</param>
		/// <param name="loginID">Login ID of a user</param>
		public void Leave(string application, string loginID)
		{
			Tracer.Trace(string.Format("Leave: Application: {0}, LoginID: {1}, Enabled: {2}", application, loginID, enable));
			if (enable)
			{
				Context context = GetContext(application);
				if (context != null)
				{
					if (context.Count > 0)
						--context.Count;
					SetContext(application, context);
				}
			}
		}
		#endregion

		#region Private Methods
		/// <summary>
		/// Get the login context for the specified application. This is stored as a cookie using the application name
		/// as the key.
		/// </summary>
		/// <remarks>
		/// The key into the cookies collection is actually composed of the application name and a GUID to render that
		/// combination unique.
		/// If the calling context is ASP, access to the cookie is a lot more involved owing to the way the cookie is
		/// accessed. It also is imperative that each and every COM interface retrieved during this process be released
		/// in the appropriate sequence failing which when IIS decides to restart/recycle the application pool, an
		/// exception will be thrown (generated while attempting to release dangling references).
		/// </remarks>
		/// <param name="application">Application key</param>
		/// <returns>Login context if found, null otherwise</returns>
		private Context GetContext(string application)
		{
			Tracer.Trace(string.Format("GetContext: Application: {0}", application));

			object count = null, loginID = null, timeStamp = null;
			HttpContext httpContext = HttpContext.Current;
			if (httpContext == null)
			{
				ASPTypeLibrary.Request request = (ASPTypeLibrary.Request)ContextUtil.GetNamedProperty("Request");
				try
				{
					ASPTypeLibrary.IRequestDictionary dictionary = request.Cookies;
					try
					{
						ASPTypeLibrary.IReadCookie readCookie = (ASPTypeLibrary.IReadCookie)dictionary[CookieKey];
						try
						{
							count = readCookie[CountKey];
							loginID = readCookie[LoginIDKey];
							timeStamp = readCookie[TimeStampKey];
						}
						finally
						{
							Marshal.ReleaseComObject(readCookie);
						}
					}
					finally
					{
						Marshal.ReleaseComObject(dictionary);
					}
				}
				finally
				{
					Marshal.ReleaseComObject(request);
				}
			}
			else
			{
				HttpCookie cookie = httpContext.Request.Cookies[CookieKey];
				if (cookie != null)
				{
					count = cookie[CountKey];
					loginID = cookie[LoginIDKey];
					timeStamp = cookie[TimeStampKey];
				}
			}

			return ((count == null) || (loginID == null) || (timeStamp == null)) ? null :
				new Context((string)loginID, DateTime.FromFileTime(Convert.ToInt64(timeStamp)), Convert.ToInt32(count));
		}

		private void LoadConfiguration()
		{
			string configFile = Assembly.GetExecutingAssembly().CodeBase; 
			Match m = (new Regex(@"file:///(?<path>.+)")).Match(configFile);
			if (!m.Success)
				throw new Exception("SSO: Failed to get assembly path");
			configFile = string.Concat(m.Groups["path"].Value.Replace('/', '\\'), ".config");

			if ((new FileInfo(configFile)).Exists)
			{
				XmlDocument doc = new XmlDocument();
				doc.Load(configFile);
				IDictionary appSettings =
					(IDictionary)(new DictionarySectionHandler()).Create(null, null, doc.GetElementsByTagName("appSettings")[0]);

				object o = appSettings["LoginLifeTime"];
				if (o != null)
					loginLifeTime = Convert.ToInt32(o);
				
				o = appSettings["CookieLifeTime"];
				if (o != null)
					cookieLifeTime = Convert.ToInt32(o);
			}
		}

		/// <summary>
		/// Set the login context of a user for the specified application.
		/// </summary>
		/// <remarks>
		/// Create a new cookie and set the login ID, count, and timestamp for that cookie based on the values stored
		/// in the login context. Set the cookie to expire in 24 hours. Save the cookie.
		/// If the calling context is ASP, access to the cookie is a lot more involved owing to the way the cookie is
		/// accessed. It also is imperative that each and every COM interface retrieved during this process be released
		/// in the appropriate sequence failing which when IIS decides to restart/recycle the application pool, an
		/// exception will be thrown (generated while attempting to release dangling references).
		/// </remarks>
		/// <param name="application">Application whose context is to be set</param>
		/// <param name="context">Context encapsulating the user info</param>
		private void SetContext(string application, Context context)
		{
			HttpContext httpContext = HttpContext.Current;
			if (httpContext == null)
			{
				ASPTypeLibrary.Response response = (ASPTypeLibrary.Response)ContextUtil.GetNamedProperty("Response");
				try
				{
					ASPTypeLibrary.IRequestDictionary dictionary = response.Cookies;
					try
					{
						ASPTypeLibrary.IWriteCookie writeCookie = (ASPTypeLibrary.IWriteCookie)dictionary[CookieKey];
						try
						{
							writeCookie[CountKey] = context.Count.ToString();
							writeCookie[LoginIDKey] = context.LoginID;
							writeCookie[TimeStampKey] = context.TimeStamp.ToFileTime().ToString();
							writeCookie.Expires = DateTime.Now.AddMinutes(cookieLifeTime);
						}
						finally
						{
							Marshal.ReleaseComObject(writeCookie);
						}
					}
					finally
					{
						Marshal.ReleaseComObject(dictionary);
					}
				}
				finally
				{
					Marshal.ReleaseComObject(response);
				}
			}
			else
			{
				HttpCookie cookie = new HttpCookie(CookieKey);
				cookie[CountKey] = context.Count.ToString();
				cookie[LoginIDKey] = context.LoginID;
				cookie[TimeStampKey] = context.TimeStamp.ToFileTime().ToString();
				cookie.Expires = DateTime.Now.AddMinutes(cookieLifeTime);

				httpContext.Response.Cookies.Add(cookie);
			}
		}
		#endregion
	}
	#endregion
}

// --- EOF ---
